/*
 * Copyright 2011 jmarsden.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cc.plural.jsonij.marshal;

import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * JavaType Utility. Queries objects for their types and manages inspector 
 * instances so time is not wasted re-inspecting the same objects.
 * 
 * Inspecting is the task of reflecting attributes and methods and their 
 * types for access.
 * 
 * @author jmarsden@plural.cc
 */
public enum JavaType {

    /**
     * The JavaTypes possible for an inspection.
     * UNKNOWN will be returned where an object cannot be decided.
     * ARRAY will be returned for an array of Objects.
     */
    BOOLEAN,
    BYTE,
    SHORT,
    INTEGER,
    FLOAT,
    DOUBLE,
    LONG,
    STRING,
    LIST,
    OBJECT,
    MAP,
    ENUM,
    ARRAY,
    ARRAY_BOOLEAN,
    ARRAY_BYTE,
    ARRAY_SHORT,
    ARRAY_INTEGER,
    ARRAY_FLOAT,
    ARRAY_DOUBLE,
    ARRAY_LONG,
    ARRAY_STRING,
    ARRAY_ENUM,
    // HMMN
    ARRAY_LIST,
    ARRAY_MAP,
    ARRAY_ARRAY,
    UNKOWN;
    boolean primitive;
    JavaArrayType arrayType;
    
    static final protected Map<Class<?>, Inspector> inspectedClasses;

    static {
        inspectedClasses = new HashMap<Class<?>, Inspector>();
    }

    JavaType() {
        primitive = true;
    }

    public void setPrimitive(boolean primitive) {
        this.primitive = primitive;
    }

    public boolean isPrimitive() {
        return this.primitive;
    }

    public static JavaType inspectObjectType(Class<?> c) {
        JavaType type = null;
        if (c == boolean.class) {
            type = JavaType.BOOLEAN;
            type.setPrimitive(true);
        } else if (c == Boolean.class) {
            type = JavaType.BOOLEAN;
            type.setPrimitive(false);
        } else if (c == int.class) {
            type = JavaType.INTEGER;
            type.setPrimitive(true);
        } else if (c == Integer.class) {
            type = JavaType.INTEGER;
            type.setPrimitive(false);
        } else if (c == double.class) {
            type = JavaType.DOUBLE;
            type.setPrimitive(true);
        } else if (c == Double.class) {
            type = JavaType.DOUBLE;
            type.setPrimitive(false);
        } else if (c == float.class) {
            type = JavaType.FLOAT;
            type.setPrimitive(true);
        } else if (c == Float.class) {
            type = JavaType.FLOAT;
            type.setPrimitive(false);
        } else if (c == long.class) {
            type = JavaType.LONG;
            type.setPrimitive(true);
        } else if (c == Long.class) {
            type = JavaType.LONG;
            type.setPrimitive(false);
        } else if (c == short.class) {
            type = JavaType.SHORT;
            type.setPrimitive(true);
        } else if (c == Short.class) {
            type = JavaType.SHORT;
            type.setPrimitive(false);
        } else if (c == byte.class) {
            type = JavaType.BYTE;
            type.setPrimitive(true);
        } else if (c == Byte.class) {
            type = JavaType.BYTE;
            type.setPrimitive(false);
        } else if (c.isEnum()) {
            type = JavaType.ENUM;
            type.setPrimitive(false);
        } else if (c == String.class || ( ( c == char.class || c == Character.class ) )) {
            type = JavaType.STRING;
            type.setPrimitive(false);
        } else if (c.isArray()) {
            type = getArrayType(c);
        }
        if (type == null) {
            if (c == List.class) {
                type = JavaType.LIST;
                type.setPrimitive(false);
            } else {
                // Test if Object is a List
                Class<?>[] interfaces = c.getInterfaces();
                for (int i = 0; i < Array.getLength(interfaces); i++) {
                    if (interfaces[i] == List.class) {
                        type = JavaType.LIST;
                        type.setPrimitive(false);
                    }
                }
            }
            Class<?>[] interfaces = c.getInterfaces();
            // Test if super classes are List
            Class<?> parent = c.getSuperclass();
            if (parent != null) {
                do {
                    interfaces = parent.getInterfaces();
                    for (int i = 0; i < Array.getLength(interfaces); i++) {
                        if (interfaces[i] == List.class) {
                            type = JavaType.LIST;
                        }
                    }
                } while (( parent = parent.getSuperclass() ) != null);
            }
        }
        if (type == null) {
            if (c == Map.class) {
                type = JavaType.MAP;
                type.setPrimitive(false);
            } else {
                // Test if Object is a List
                Class<?>[] interfaces = c.getInterfaces();
                for (int i = 0; i < Array.getLength(interfaces); i++) {
                    if (interfaces[i] == Map.class) {
                        type = JavaType.MAP;
                        type.setPrimitive(false);
                    }
                }
            }
            Class<?>[] interfaces = c.getInterfaces();
            Class<?> parent = c.getSuperclass();
            if (parent != null) {
                do {
                    interfaces = parent.getInterfaces();
                    for (int i = 0; i < Array.getLength(interfaces); i++) {
                        if (interfaces[i] == Map.class) {
                            type = JavaType.MAP;
                        }
                    }
                } while (( parent = parent.getSuperclass() ) != null);
            }
        }
        if (type == null) {
            if (isObjectType(c)) {
                type = JavaType.OBJECT;
                type.setPrimitive(false);
            }
        }
        if (type != null) {
            return type;
        } else {
            return JavaType.UNKOWN;
        }
    }

    public static Inspector getInstpector(Class<?> objectClass) {
        Inspector inspector = null;
        synchronized (inspectedClasses) {
            if (inspectedClasses.containsKey(objectClass)) {
                inspector = inspectedClasses.get(objectClass);
            } else {
                inspector = new Inspector(objectClass);
                inspector.inspect();
                inspectedClasses.put(objectClass, inspector);
            }
        }
        return inspector;
    }

    public static boolean isObjectType(Class<?> objectClass) {
        Inspector inspector = getInstpector(objectClass);
        InspectorProperty[] properties = inspector.getProperties();
        if (( properties != null && Array.getLength(properties) > 0 ) || inspector.hasInnerArray() || inspector.hasInnerObject()) {
            return true;
        }
        return false;
    }

    @SuppressWarnings("unused")
	public static JavaType getArrayType(Class<?> objectClass) {
        JavaType resultType = null;
        Class<?> objectComponentType = objectClass.getComponentType();
        
        if (!objectComponentType.isArray()) {
            
            
            JavaType componentType = inspectObjectType(objectComponentType);
            //resultType.arrayType = new JavaArrayType(objectClass);
            switch (componentType) {
                case BOOLEAN:
                    resultType = JavaType.ARRAY_BOOLEAN;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case BYTE:
                    resultType = JavaType.ARRAY_BYTE;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case INTEGER:
                    resultType = JavaType.ARRAY_INTEGER;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case SHORT:
                    resultType = JavaType.ARRAY_SHORT;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case FLOAT:
                    resultType = JavaType.ARRAY_FLOAT;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case LONG:
                    resultType = JavaType.ARRAY_LONG;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case DOUBLE:
                    resultType = JavaType.ARRAY_DOUBLE;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case STRING:
                    resultType = JavaType.ARRAY_STRING;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case ENUM:
                    resultType = JavaType.ARRAY_ENUM;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case MAP:
                    resultType = JavaType.ARRAY_MAP;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case LIST:
                    resultType = JavaType.ARRAY_LIST;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
                case OBJECT:
                    resultType = JavaType.ARRAY;
                    resultType.setPrimitive(componentType.isPrimitive());
                    break;
            }
        } else {
            resultType = JavaType.ARRAY_ARRAY;
            resultType.setPrimitive(false);
        }
        
        if (resultType != null) {
            return resultType;
        } else {
            return JavaType.UNKOWN;
        }
    }
    
    public static class JavaArrayType {
        Class<?> arrayType;
        int dimension;
        
        public JavaArrayType(Class<?> arrayType) {
            this.arrayType = arrayType;
            dimension = 0;
            this.inspect();
        }
        
        public final void inspect() {
            if(arrayType != null && arrayType.isArray()) {
                Class<?> innerType = arrayType.getComponentType();
                dimension = 1;
                while(innerType.isArray() && (innerType = arrayType.getComponentType()) != null) {
                    dimension++;
                }
            }
        }

        @Override
        public String toString() {
            return "Array " + arrayType.getSimpleName() + "[" + dimension + "]";
        }       
    }
}
